package com.yixing.tech.common.base;

import com.alibaba.fastjson.JSONArray;
import com.yixing.tech.common.enums.ComparisonSymbol;
import com.yixing.tech.common.enums.YesOrNo;
import com.yixing.tech.common.utils.DataUtil;
import com.mongodb.BasicDBObject;
import com.mongodb.client.AggregateIterable;
import com.mongodb.client.MongoCursor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.lang3.StringUtils;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.BulkOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.index.IndexDefinition;
import org.springframework.data.mongodb.core.index.IndexInfo;
import org.springframework.data.mongodb.core.index.TextIndexDefinition;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Repository;
import org.springframework.util.ObjectUtils;

import java.beans.PropertyDescriptor;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author yixing tech
 * at 2021/12/17
 * describe：
 */
@Repository
@Slf4j
public class BaseMongoDao {

    @Autowired
    private MongoTemplate mongoTemplate;


    /**
     * 判断是否存在
     *
     * @param condition
     * @param c
     */
    public boolean exists(BaseVO condition, Class c) {
        Query query = new Query();
        query.addCriteria(condition2Criteria(condition));
        return mongoTemplate.exists(query, c);
    }

    /**
     * 判断是否存在
     *
     * @param condition
     * @param tableName
     */
    public boolean exists(BaseVO condition, String tableName) {
        Query query = new Query();
        query.addCriteria(condition2Criteria(condition));
        return mongoTemplate.exists(query, tableName);
    }


    /**
     * save
     *
     * @param po
     */
    public <T> T save(T po) {
        return mongoTemplate.save(po);
    }


    /**
     * save
     *
     * @param po
     */
    public <T> void saveChild(String parentId, T po) {
        T t = mongoTemplate.save(po);
        Query query = new Query(Criteria.where("id").is(parentId));
        Update update = new Update();
        update.addToSet("children", t);
        mongoTemplate.upsert(query, update, po.getClass());
    }


    public <T> void batchSave(List<? extends T> list) {
        mongoTemplate.insert(list);
    }

    public <T> Collection<T> batchSave(Collection<? extends T> batchToSave, Class<?> entityClass) {
        return mongoTemplate.insert(batchToSave, entityClass);
    }

    /**
     * count
     */

    public <T> long count(Criteria criteria, Class<T> t) {
        Query query = new Query(criteria);
        return mongoTemplate.count(query, t);
    }

    public long count(Criteria criteria, String tableName) {
        Query query = new Query(criteria).maxTime(Duration.ofSeconds(10));
        return mongoTemplate.count(query,tableName);
    }

    /**
     * get
     */
    public <T> T get(Object id, Class<T> c) {
        return mongoTemplate.findById(id, c);
    }

    /**
     * @param ids
     * @param t
     * @param <T>
     */
    public <T, V> List<V> batchQuery(List<String> ids, Class<T> t, Class<V> v) {
        Query query = new Query(Criteria.where("id").in(ids)).maxTime(Duration.ofSeconds(10));
        List<T> list = mongoTemplate.find(query, t);
        return list.stream().map(m -> DataUtil.o2o(m, v)).collect(Collectors.toList());

    }

    /**
     * update
     */
    public void update(BasePO po) {
        Query query = new Query();
        query.addCriteria(Criteria.where("id").is(po.getId()));
        Update update = new Update();
        try {
            Map map = DataUtil.objectToMap(po);

            for (Object key : map.keySet()) {
                Object value = map.get(key);
                if (null == value) continue;
                if (value instanceof BasePO) {
                    BasePO v = (BasePO) value;
                    if (StringUtils.isEmpty(v.getId())) {
                        update.unset(key.toString());
                        continue;
                    }
                }
                update.set(key.toString(), value);

            }
            mongoTemplate.upsert(query, update, po.getClass());
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }

    }

    /**
     * update
     */
    public void update(Criteria criteria, Update update, Class c) {
        Query query = new Query();
        query.addCriteria(criteria);
        mongoTemplate.upsert(query, update, c);
    }


    /**
     * batch update no null by ids
     *
     * @param c
     * @param updates
     */
    public void batchUpdateNoNullValueByIds(Class c, List<BasePO> updates) {
        if (updates == null) return;
        List<Pair<Query, Update>> list = new ArrayList<>();
        BulkOperations operations = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, c);
        for (BasePO po : updates) {
            Query query = Query.query(Criteria.where("id").is(new ObjectId(po.getId())));
            Update update = new Update();
            try {
                Map map = DataUtil.objectToMap(po);
                for (Object key : map.keySet()) {
                    Object value = map.get(key);
                    if (null == value) continue;
                    update.set(key.toString(), value);
                }
                Pair<Query, Update> pair = Pair.of(query, update);
                list.add(pair);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        operations.updateMulti(list);
        operations.execute();
    }

    /**
     * @param ids
     * @param update
     * @param t
     * @param <T>
     */
    public <T> void batchUpdate(List ids, Update update, Class<T> t) {

        Criteria where = Criteria.where("_id").in(ids);
        Query query = new Query(where);
        mongoTemplate.updateMulti(query, update, mongoTemplate.getCollectionName(t));

    }


    /**
     * delete
     */
    public void delete(String id, Class c) {
        Object o = get(id, c);
        if (o != null) {
            mongoTemplate.remove(o);
        }
    }

    public void delete(List<String> ids, Class c) {
        Query query = new Query();
        query.addCriteria(Criteria.where("id").in(ids));
        mongoTemplate.remove(query, c);
    }

    /**
     * listAll
     */
    public <T> List<T> listAll(Class<T> c) {
        return mongoTemplate.findAll(c);
    }

    /**
     * listAll
     */
    public <T> List<T> listAll(Class<T> c, Criteria criteria) {
        Query query = new Query();
        query.addCriteria(criteria).allowDiskUse(true).maxTime(Duration.ofSeconds(10));
        return mongoTemplate.find(query, c);
    }
    public <T> List<T> listAll(Class<T> c, Criteria criteria,Sort sort) {
        Query query = new Query();
        query.addCriteria(criteria);
        query.with(sort).allowDiskUse(true).maxTime(Duration.ofSeconds(10));
        return mongoTemplate.find(query, c);
    }

    /**
     * queryOne
     */
    public <T> T queryOne(Class<T> c, Criteria criteria) {
        Query query = new Query();
        query.addCriteria(criteria).maxTime(Duration.ofSeconds(10));
        return mongoTemplate.findOne(query, c);
    }

    /**
     * queryOne
     */
    public <T> T queryOne(Class<T> c, BaseVO vo) {
        Query query = new Query();
        query.addCriteria(condition2Criteria(vo)).maxTime(Duration.ofSeconds(10));
        return mongoTemplate.findOne(query, c);
    }

    /**
     * queryOne
     */
    public <T, V> V queryOne(Class<T> c, Class<V> v, BaseVO vo) {
        Query query = new Query();
        query.addCriteria(condition2Criteria(vo)).maxTime(Duration.ofSeconds(10));
        return DataUtil.o2o(mongoTemplate.findOne(query, c), v);
    }

    /**
     * queryOne
     */
    public <T, V> V queryOne(String tableName, Class<V> v, BaseVO vo) {
        Query query = new Query();
        query.addCriteria(condition2Criteria(vo)).maxTime(Duration.ofSeconds(10));
        return mongoTemplate.findOne(query, v, tableName);
    }

    /**
     * pageAll
     */
    public <T> Page<T> listPage(PageRequest pageRequest, Class<T> c) {
        return listPage(pageRequest, c, null);
    }

    /**
     * pageAll
     */
    public <T> Page<T> listPage(PageRequest pageRequest, Class<T> c, Criteria criteria) {
        Query query = new Query();
        query.with(pageRequest);
        Query query1 = new Query();
        if (criteria != null) {
            query.addCriteria(criteria);
            query1.addCriteria(criteria);
        }
        long count = mongoTemplate.count(query1.maxTime(Duration.ofSeconds(10)), c);
        List<T> list = mongoTemplate.find(query.maxTime(Duration.ofSeconds(10)), c);
        return new Page<>(list, pageRequest.getPageSize(), pageRequest.getPageNumber() + 1, count);
    }

    public <T, V> Page<V> listPage(PageRow<? extends BaseVO> pageRow, Class<T> p, Class<V> v) {
        PageRequest pageRequest;
        BaseVO.Sort bSort = pageRow.getCondition().getSortCondition();
        if (bSort != null && !StringUtils.isEmpty(bSort.getCode())) {
            Sort sort = Sort.by(Sort.Direction.fromString(bSort.getDirection()), bSort.getCode());
            pageRequest = PageRequest.of(pageRow.getPageNo(), pageRow.getPageSize(), sort);
        } else {
            pageRequest = PageRequest.of(pageRow.getPageNo(), pageRow.getPageSize());
        }

        Criteria criteria = condition2Criteria(pageRow.getCondition());

        Page<T> page = listPage(pageRequest, p, criteria);
        List<T> list = page.getData();
        List<V> listVO = list.stream().map(m -> DataUtil.o2o(m, v)).collect(Collectors.toList());
        return new Page<>(listVO, page.getPageSize(), page.getPageNo(), page.getTotalCount());
    }


    public <T, V> List<V> list(Class<T> t, Class<V> v) {
        List<T> list = listAll(t);
        return list.stream().map(m -> DataUtil.o2o(m, v)).collect(Collectors.toList());
    }


    public <T, V> List<V> list(BaseVO condition, Class<T> t, Class<V> v) {
        Criteria criteria = condition2Criteria(condition);
        List<T> list;
       BaseVO.Sort s =  condition.getSortCondition();

       if(s!=null&&s.getDirection()!=null){
           list = listAll(t, criteria,Sort.by(Sort.Direction.fromString(s.getDirection()),s.getCode()));
       }else{
           list = listAll(t, criteria);
       }
        return list.stream().map(m -> DataUtil.o2o(m, v)).collect(Collectors.toList());
    }


    public Criteria condition2Criteria(BaseVO condition) {
        Criteria criteria = new Criteria();
        if (condition == null) {
            return criteria;
        }
        List<Criteria> criteriaList = condition2CriteriaList(condition);
        if (criteriaList.size() > 0) {
            criteria.andOperator(criteriaList);
        }
        return criteria;
    }


    /**
     * 通用对象属性转sql条件语句（and 语句）。
     *
     * @param condition
     * @return
     */
    private List<Criteria> condition2CriteriaList(BaseVO condition) {
        List<Criteria> list = new ArrayList<>();
        PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
        PropertyDescriptor[] descriptors = propertyUtilsBean.getPropertyDescriptors(condition);
        Map<String, ComparisonSymbol> symbols = condition.getParamComparisonSymbols();
        for (int i = 0; i < descriptors.length; i++) {
            String key = descriptors[i].getName();
            if ("class".equals(key)) {
                continue;
            }
            if ("paramComparisonSymbols".equals(key)) {
                continue;
            }
            if ("paramsCondition".equals(key)) {
                continue;
            }
            if ("sortCondition".equals(key)) {
                continue;
            }
            if ("sortConditions".equals(key)) {
                continue;
            }
            try {
                Object value = propertyUtilsBean.getNestedProperty(condition, key);
                if (value instanceof BaseVO) {
                    BaseVO vo = (BaseVO) value;
                    if (null != vo.getId()) {
                        key += "._id";
                        value = new ObjectId(vo.getId());
                    }

                }
                ComparisonSymbol symbol = symbols.getOrDefault(key, ComparisonSymbol.is);
                Criteria criteria;
                if (symbol == ComparisonSymbol.not) {
                    criteria = Criteria.where(key).ne(null);
                } else if (symbol == ComparisonSymbol.isNull) {
                    criteria = Criteria.where(key).isNull();
                } else {
                    if (symbol != ComparisonSymbol.between && symbol != ComparisonSymbol.in&& symbol != ComparisonSymbol.nin) {
                        if (null == value || StringUtils.isBlank(value.toString()) || "null".equals(value.toString())) {
                            continue;
                        }
                    } else {
                        value = condition.getParamsCondition().get(key);
                    }
                    criteria = buildCriteria(symbol, key, value);
                }
                list.add(criteria);
            } catch (Exception e) {
                e.printStackTrace();
                log.error(e.getMessage());
            }
        }
        return list;
    }

    /**
     * 通用对象属性转sql条件语句（and 语句）。
     *
     */
    public List<Criteria> condition2CriteriaList(ConditionsVO conditions) {
        List<Criteria> list = new ArrayList<>();
        list.add(Criteria.where("isDelete").ne(YesOrNo.YES.ordinal()));
        if(conditions != null) {
            String tenantId = conditions.getTenantId();
            if (!StringUtils.isEmpty(tenantId)) {
                list.add(Criteria.where("tenantId").is(tenantId));
            }
            list.addAll(condition2CriteriaList(conditions.getConditions()));
        }
        return list;
    }
    public List<Criteria> condition2CriteriaList(List<ConditionsVO.Condition> conditionList) {
        List<Criteria> list = new ArrayList<>();
        if (conditionList == null) return list;
        for (ConditionsVO.Condition cond : conditionList) {
            ComparisonSymbol symbol = ComparisonSymbol.valueOf(cond.getSymbol());
            String key = cond.getKey();
            Object value = cond.getValue();
            if(!ObjectUtils.isEmpty(value)&&value.toString().startsWith("$"))continue;
            if (key.contains(".id") || key.contains("._id") || "id".equals(key) || "_id".equals(key)) {
                if (symbol == ComparisonSymbol.in|| symbol == ComparisonSymbol.nin) {
                    if (value != null) {
                        List<Object> values = (List<Object>) value;
                        List<ObjectId> newValues = new ArrayList<>();
                        for (Object m : values) {
                            if(m instanceof ObjectId) {
                                // 兼容
                                newValues.add((ObjectId) m);
                            } else {
                                newValues.add(new ObjectId(m.toString()));
                            }
                        }
                        value = newValues;
                    }

                } else {
                    value = value == null ? null : new ObjectId(value.toString());
                }
            }

            Criteria criteria;
            if (symbol == ComparisonSymbol.not) {
                criteria = Criteria.where(key).ne(null);
            } else if (symbol == ComparisonSymbol.isNull) {
                criteria = Criteria.where(key).isNull();
            } else {
                if (value == null) continue;
                criteria = buildCriteria(symbol, key, value);
            }
            list.add(criteria);
        }
        return list;
    }
    /**
     * build criteria
     *
     * @param symbol
     * @param key
     * @param value
     * @return
     */
    public Criteria buildCriteria(ComparisonSymbol symbol, String key, Object value) {
        if (value instanceof Boolean) {
            if (symbol == ComparisonSymbol.is && !((Boolean) value)) {
                symbol = ComparisonSymbol.ne;
                value = true;
            }
        }
        Criteria criteria;
        switch (symbol) {
            case gt:
                criteria = Criteria.where(key).gt(value);
                break;
            case gte:
                criteria = Criteria.where(key).gte(value);
                break;
            case lt:
                criteria = Criteria.where(key).lt(value);
                break;
            case lte:
                criteria = Criteria.where(key).lte(value);
                break;
            case ne:
                criteria = Criteria.where(key).ne(value);
                break;
            case in:
                criteria = Criteria.where(key).in((List) value);
                break;
            case nin:
                criteria = Criteria.where(key).nin((List) value);
                break;
            case like:
                Pattern pattern = Pattern.compile("^.*" + value.toString() + ".*$", Pattern.CASE_INSENSITIVE);
                criteria = Criteria.where(key).regex(pattern);
                break;
            case between:
                List<Object> v = (List<Object>) value;
                criteria = Criteria.where(key).gte(v.get(0)).lte(v.get(1));
                break;
            case contain:
                criteria = Criteria.where(key).all(value);
                break;
            default:
                criteria = Criteria.where(key).is(value);
                break;
        }
        return criteria;
    }


    public <T> AggregationResults<T> aggregate(List<AggregationOperation> operations, String collectionName, Class<T> outputType) {
        Aggregation aggregation = Aggregation.newAggregation(operations).withOptions(AggregationOptions.builder().allowDiskUse(true).maxTime(Duration.ofSeconds(10)).build());
        return mongoTemplate.aggregate(aggregation, collectionName, outputType);
    }

    /**
     * mongo管道聚合操作原生sql转指令
     *
     * @param mongoSql
     */
    public <T> List<Document> executeAggreSql(String mongoSql, Class<T> t) {
        List<BasicDBObject> basicDBObjectList = JSONArray.parseArray(mongoSql, BasicDBObject.class);
        AggregateIterable<Document> aggregate = mongoTemplate.getDb().getCollection(mongoTemplate.getCollectionName(t)).aggregate(basicDBObjectList);

        MongoCursor<Document> mongoCursor = aggregate.iterator();
        List<Document> documentList = new ArrayList<>(1000);
        while (mongoCursor.hasNext()) {
            Document doc = mongoCursor.next();
            documentList.add(doc);
        }
        return documentList;
    }

    /**
     * 全字段搜索索引
     *
     * @param tableName
     */
    public void insertTextIndex(String tableName) {
        IndexDefinition indexDefinition = TextIndexDefinition.forAllFields();
        mongoTemplate.indexOps(tableName).ensureIndex(indexDefinition);
    }

    /**
     * hash index
     * @param tableName
     * @param indexes
     */
    public void insertIndex(String tableName,List<String> indexes){
        List<IndexInfo> indexList = mongoTemplate.indexOps(tableName).getIndexInfo();
        List<String> indexStrs = indexList.stream().map(IndexInfo::getName).collect(Collectors.toList());
        for(String index:indexes){
            if(indexStrs.contains(index+"_1"))continue;
            mongoTemplate.indexOps(tableName).ensureIndex(new IndexDefinition() {
                @Override
                public Document getIndexKeys() {
                    return new Document(index, 1);
                }

                @Override
                public Document getIndexOptions() {
                    return new Document("background", true);
                }
            });
        }
    }


}
